/*
 * Copyright 2009, Team Mondegreen:
 * Brian Hsieh - bh2181@columbia.edu
 * Eugene Yao - eyy2102@columbia.edu
 * Jake Amonette - jaa2149@columbia.edu
 * Tal Giat - tg2235@colubia.edu
 * 
 * This file is part of AMFM (A Minimal Fractal Music Language), a language created for
 * fractal music as part of the Programming Languages and Translator class at Columbia university.
 *
 * For more details go to: http://code.google.com/p/amfm/
 * 
 * AMFM is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * at your option any later version.
 * 
 * AMFM is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with AMFM.  If not, see <http://www.gnu.org/licenses/>. 
 */

package org.mondegreen.amfm.compiler;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.tree.CommonTreeNodeStream;
import org.antlr.runtime.tree.Tree;
import org.antlr.stringtemplate.StringTemplate;
import org.antlr.stringtemplate.StringTemplateGroup;

import com.myronmarston.music.Instrument;

/**
 * AMFM compiler - takes amfm code and generates gave code from it
 * 
 * @author talgiat
 */
public class AmFmCompiler {
  public static final String AUDIO_FILE_EXTENSION = "wav";
  
  // lookup between the language instruments and the ones the computer supports 
  private final Map<String,String> instrumentsMap;
  private final AmFmErrorManager errorManager;
  
  public AmFmCompiler() {
    instrumentsMap = new HashMap<String,String>();
    // Initialize lookup
    instrumentsMap.put("PIANO", Instrument.DEFAULT.getName()); // default is type of piano
    instrumentsMap.put("VIOLIN", "Violin");
    instrumentsMap.put("CELLO", "Cello");
    instrumentsMap.put("TRUMPET", "Trumpet");
    instrumentsMap.put("VIOLA", "Viola");
    instrumentsMap.put("FLUTE", "Flute");
    
    errorManager = new AmFmErrorManager();
  }

  /**
   * Compiles an AmFm program into a Java class that can be run to generate the final audio wav file for the piece.
   * 
   * @param args an array of arguments the only one should be a name of the source program to compile, with full or relative path
   * @throws Exception 
   */
  public static void main(String[] args) throws Exception {
    if (args == null || args.length == 0) {
      System.err.println("You must pass a source AmFm program file as an argument");
      System.err.println("Usage: amfm filename");
      System.exit(1);
    }
    
    AmFmCompiler compiler = new AmFmCompiler();
    File source = new File(args[0]);
    boolean successfulCompilation = compiler.compile(source);
    if (!successfulCompilation) {
      compiler.printErrorMessages();
      System.exit(1);
    } else {
      // Compile output source code file
      String outputClassname = compiler.getOutputClassName(source);
      String outputParentForCP = source.getParent()!=null ? source.getParent() : ".";
      String outputParent = source.getParent()!=null ? source.getParent()+ File.separator : "";
      String fulloutputFilename =  outputParent + outputClassname;
      String libFolder = "lib" + File.separator;
      // Setting classpath
      String classPath = "-cp " + 
      libFolder + "antlrworks-1.2.3-amfm.jar" + File.pathSeparator + 
      libFolder + "tritonus_share-0.3.6.jar" + File.pathSeparator + 
      libFolder + "gervill.jar" + File.pathSeparator + 
      libFolder +  "simple-xml-1.8.jar" + File.pathSeparator + 
      libFolder +  "stax-1.2.0.jar" + File.pathSeparator + 
      libFolder + "stax-api-1.0.1.jar" + File.pathSeparator + 
      libFolder + "amfm-1.0.jar" + File.pathSeparator + 
      outputParentForCP;
      
      String compileCommand = "javac " + classPath + " " + fulloutputFilename + ".java";
      try {
        executeCommand(compileCommand);
        String runCommand = "java " + classPath + " " + fulloutputFilename;
        executeCommand(runCommand);
        System.out.println("Compilation finished successfully, generated file: " + fulloutputFilename + ".wav");
      } catch (IOException e) {
        System.err.println(":( - Couldn't compile generated program");
      }
      
    }
    
  }
  
  /**
   * Executes external command
   * @param command the command to execute 
   * @throws Exception if there was an error getting the system process to execute the command with
   */
  private static void executeCommand(String command) throws Exception { 
    Process process = Runtime.getRuntime().exec(command);
    process.waitFor();
    BufferedReader commandErrorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    StringBuilder commandErrors = new StringBuilder();
    String tmp = null;
    while((tmp = commandErrorReader.readLine()) != null) {
      commandErrors.append(tmp).append("\n");
    }
    if (commandErrors.length() > 0) {
      System.out.println(commandErrors);
    }
  }
  
  
  /**
   * Compiles an AMFM program given a source string. Output will be a Java class in the given output path and
   * with the given class name. Return true if compilation was successful, if not false. In the case of failure,
   * called should call getErrorMessage to get a list of the error messages that occured during compilation.
   * 
   * @param source a string containing amfm source
   * @param outputPath the path to write the output java class
   * @param outputClassname the java class name to generate based on the given source
   * @return true if compilation was successful, false otherwise.
   */
  public boolean compile(String source, String outputPath, String outputClassname) {
    
    // Clear all error messages in the manager before we start compiling
    errorManager.resetMessages();
    
    // USE LEXER TO GENERATE ALL TOKENS FROM SOURCE
    ANTLRStringStream input = new ANTLRStringStream(source);
    AmFmLexer lexer = new AmFmLexer(input);
    lexer.setErrorManager(errorManager);

    // PARSE INPUT AND BUILD AST

    // Create a stream of tokens pulled from the lexer
    CommonTokenStream tokens = new CommonTokenStream(lexer);
    AmFmParser parser = new AmFmParser(tokens);
    parser.setErrorManager(errorManager); // This is a must so parser can resister error messages
    AmFmParser.program_return parserResult = null;
    try {
      // Parse token stream, and do syntactic and semantic validation
      parserResult = parser.program();
      if (errorManager.hasErrorMessages()) { // If we encountered errors during parsing, return indicating compilation has failed
        return false; 
      }
    } catch (RecognitionException e) {
      // Shouldn't get here since errors are caught by antlr and recovered
      return false;
    }

    // FIRST TREE WALK - CODE OPTIMIZER (REMOVES UNUSED SEEDS SUTTREES)

    // Create tree node stream from parser AST to feed into optimizer
    CommonTreeNodeStream parserNodes = createTreeNodeStream((Tree) parserResult.getTree(), tokens);

    AmFmOptimizer optimizer = new AmFmOptimizer(parserNodes);
    optimizer.setUsedSeeds(parser.getUsedSeeds());
    AmFmOptimizer.program_return optimizerResult = null;
    try {
      // Remove unused seeds from the AST
      optimizerResult = optimizer.program();
    } catch (RecognitionException e) {
      // Shouldn't get here since errors are caught by antlr and recovered
      return false;
    }

    // SECOND TREE WALK - GENERATE JAVA CODE (USING STRINGTEMPLATE)

    // Load code templates file
    StringTemplateGroup templates = null;
    try {
      InputStreamReader templatesReader = new InputStreamReader(getClass().getResourceAsStream("AmFm.stg"));
      templates = new StringTemplateGroup(templatesReader);
      templatesReader.close();
    } catch (IOException e) {
      errorManager.addError(AmFmErrorCode.FAILED_READING_CODE_TEMPLATES,"AmFm.stg");
      return false;
    }

    // Create tree node stream from trimmed AST to feed into code generator
    CommonTreeNodeStream nodes = createTreeNodeStream((Tree) optimizerResult.getTree(), tokens);
    
    String filePath = outputPath != null ? outputPath + File.separator : "";
    AmFmCodeGenerator generator = new AmFmCodeGenerator(nodes);
    generator.setInstrumentsMap(instrumentsMap); // set lookup for instruments
    generator.setTemplateLib(templates);
    AmFmCodeGenerator.program_return generatorResult = null;
    try {
      String paddedFilePath = File.separator.equals("\\") ? filePath.replaceAll("\\\\", "\\\\\\\\"): filePath;
      // Write output java file using the code generator
      generatorResult = generator.program(paddedFilePath,outputClassname);
    } catch (RecognitionException e) {
      // Shouldn't get here since errors are caught by antlr and recovered
      return false;
    }

    // SAVE CODE TO JAVA FILE
    StringTemplate output = (StringTemplate) generatorResult.getTemplate();
    String javaFilename = filePath + outputClassname + ".java";
    try {
      RandomAccessFile javaFile = new RandomAccessFile(javaFilename, "rw");
      javaFile.setLength(0);
      javaFile.write(output.toString().getBytes());
      javaFile.close();
    } catch (Exception e) {
      errorManager.addError(AmFmErrorCode.FAILED_WRITING_JAVA_CLASS, javaFilename);
      return false;
    }
    
    return true;

  }

  /**
   * Compiler AMFM program given a source file, output will be a Java class in the same folder the source file
   * is in with the same name as the source file but (of course) with the .java extension. For example if
   * source file is MyMasterpiece.amfm output will be MyMasterpiece.java
   * 
   * @param source the source file to compile
   */
  public boolean compile(File source) {
    
    // Read source content to a string
    String amfmSource = null;
    try {
      RandomAccessFile sourceFile = new RandomAccessFile(source, "r");
      byte[] sourceContent = new byte[(int) sourceFile.length()];
      sourceFile.readFully(sourceContent);
      sourceFile.close();
      amfmSource = new String(sourceContent);
    } catch (IOException e) {
      errorManager.addError(AmFmErrorCode.FAILED_OPENING_SOURCE_FILE, source.getAbsoluteFile());
      return false;
    }
    
    String outputClassName = getOutputClassName(source);
    
    // Clean old output if exists (java source, class and audio file)
    String folder = source.getParent() != null ? source.getParent() + File.separator : "";
    String javaSourceFilename = folder + outputClassName + ".java";
    new File(javaSourceFilename).delete();
    String javaClassFilename = folder + outputClassName + ".class";
    new File(javaClassFilename).delete();
    String audioFilename = folder + outputClassName + "." + AUDIO_FILE_EXTENSION;
    new File(audioFilename).delete();
    
    return compile(amfmSource, source.getParent(), outputClassName);
  }

  /**
   * Returns a list of the error message that were registered during compilation according to the
   * order they occurred during compilation.
   * @return an unmodifiable list of error messages 
   */
  public List<String> getErrorMessages() {
    return errorManager.getErrorMessages();
  }
  
  /**
   * Prints all the compilation error messages to the "standard" error stream
   */
  public void printErrorMessages() {
    List<String> compilationErrors = getErrorMessages();
    for (String compilationError: compilationErrors) {
      System.err.println(compilationError);
    }
  }

  private CommonTreeNodeStream createTreeNodeStream(Tree ast, CommonTokenStream tokens) {
    CommonTreeNodeStream parserNodes = new CommonTreeNodeStream(ast);
    parserNodes.setTokenStream(tokens);
    return parserNodes;
  }
  
  private String getOutputClassName(File source) {
    // figure out output class name based on input source
    String sourceFilename = source.getName();
    String outputClassName;
    int extensionIndex = sourceFilename.lastIndexOf(".");
    if (extensionIndex != -1) { // has extension 
      outputClassName = sourceFilename.substring(0, extensionIndex);
    } else {
      outputClassName = sourceFilename;
    }
    return outputClassName;
  }
}